﻿using System;
using System.Collections.Generic;
using System.Diagnostics;

using System.ComponentModel;

using System.Windows;
using System.Reflection;

using System.Linq;
using System.Linq.Expressions;


namespace DailyFx.UI
{
	/// <summary> 
	///		Monitors the PropertyChanged event of an object that implements INotifyPropertyChanged,
	///		and executes callback methods (i.e. handlers) registered for properties of that object.
	/// </summary>
	public class PropertyObserver<T> : IWeakEventListener where T : class, INotifyPropertyChanged
	{
		private readonly Dictionary<string, Action<T>> mPropertyToHandler;
		private readonly WeakReference mPropertyReference;

		/// <summary>
		/// 
		/// </summary>
		public PropertyObserver( T propertySource)
		{
			if (propertySource == null)
				throw new ArgumentNullException("propertySource");

			mPropertyReference = new WeakReference(propertySource);
			mPropertyToHandler = new Dictionary<string, Action<T>>();
		}

		/// <summary>
		///		Registers a callback to be invoked when the PropertyChanged event has been raised for the specified property.
		/// </summary>
		public PropertyObserver<T> RegisterHandler( Expression<Func<T, object>> expression, Action<T> handler)
		{
			if (expression == null)
				throw new ArgumentNullException("expression");

			string propertyName = GetPropertyName(expression);
			if (String.IsNullOrEmpty(propertyName))
				throw new ArgumentException("'expression' did not provide a property name.");

			if (handler == null)
				throw new ArgumentNullException("handler");

			return RegisterHandler( propertyName, handler);
		}

		/// <summary>
		///		Registers a callback to be invoked when the PropertyChanged event has been raised for the specified property.
		/// </summary>
		public PropertyObserver<T> RegisterHandler ( string propertyName, Action<T> handler)
		{
			var propertySource = this.GetPropertyReference();
			if (propertySource != null)
			{
				mPropertyToHandler[propertyName] = handler;
				PropertyChangedEventManager.AddListener(propertySource, this, propertyName);
			}

			return this;
		}

		/// <summary>
		///		Removes the callback associated with the specified property.
		/// </summary>
		public PropertyObserver<T> UnregisterHandler( Expression<Func<T, object>> expression)
		{
			if (expression == null)
				throw new ArgumentNullException("expression");

			string propertyName = GetPropertyName(expression);
			if (String.IsNullOrEmpty(propertyName))
				throw new ArgumentException("'expression' did not provide a property name.");

			return UnregisterHandler(propertyName);
		}


		/// <summary>
		///		Removes the callback associated with the specified property.
		/// </summary>
		public PropertyObserver<T> UnregisterHandler( string propertyName)
		{
			T propertySource = GetPropertyReference();
			if (propertySource != null)
			{
				if (mPropertyToHandler.ContainsKey(propertyName))
				{
					mPropertyToHandler.Remove(propertyName);
					PropertyChangedEventManager.RemoveListener(propertySource, this, propertyName);
				}
			}

			return this;
		}

		/// <summary>
		///		Gets the name of the property.
		/// </summary>
		private static string GetPropertyName( Expression<Func<T, object>> expression)
		{
			var lambda = expression as LambdaExpression;
			MemberExpression memberExpression;
			if (lambda.Body is UnaryExpression)
			{
				var unaryExpression = lambda.Body as UnaryExpression;
				memberExpression = unaryExpression.Operand as MemberExpression;
			}
			else
			{
				memberExpression = lambda.Body as MemberExpression;
			}

			Debug.Assert(memberExpression != null, "Please provide a lambda expression like 'n => n.PropertyName'");

			if (memberExpression != null)
			{
				var propertyInfo = memberExpression.Member as PropertyInfo;
				if (propertyInfo != null)
					return propertyInfo.Name;
			}

			return null;
		}

		/// <summary>
		/// Gets the property source.
		/// </summary>
		/// <returns></returns>
		private T GetPropertyReference ()
		{
			try
			{
				return (T)mPropertyReference.Target;
			}
			catch
			{
				return default(T);
			}
		}

		/// <summary>
		/// Receives events from the centralized event manager.
		/// </summary>
		/// <param name="managerType">The type of the <see cref="T:System.Windows.WeakEventManager"/> calling this method.</param>
		/// <param name="sender">Object that originated the event.</param>
		/// <param name="e">Event data.</param>
		/// <returns>
		/// true if the listener handled the event. It is considered an error by the <see cref="T:System.Windows.WeakEventManager"/> handling in WPF to register a listener for an event that the listener does not handle. Regardless, the method should return false if it receives an event that it does not recognize or handle.
		/// </returns>
		bool IWeakEventListener.ReceiveWeakEvent( Type managerType, object sender, EventArgs e)
		{
			if (managerType == typeof(PropertyChangedEventManager))
			{
				string propertyName = ((PropertyChangedEventArgs)e).PropertyName;
				var propertySource = (T)sender;

				if (String.IsNullOrEmpty(propertyName))
				{
					// When the property name is empty, all properties are considered to be invalidated.
					// Iterate over a copy of the list of handlers, in case a handler is registered by a callback.
					mPropertyToHandler
						.Values
						.ToList()
						.ForEach(h => h(propertySource));
					return true;
				}

				Action<T> handler;
				if (mPropertyToHandler.TryGetValue(propertyName, out handler))
				{
					handler(propertySource);
					return true;
				}
			}

			return false;
		}
	}

	
}